<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="description" content="迎着朝阳的博客" />
  
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    Mysql架构与执行流程 |  迎着朝阳
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="https://dxysun.com/static/yan.png" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  
<script>
var _hmt = _hmt || [];
(function() {
	var hm = document.createElement("script");
	hm.src = "https://hm.baidu.com/hm.js?aa994a8d65700b8835787dd39d079d7e";
	var s = document.getElementsByTagName("script")[0]; 
	s.parentNode.insertBefore(hm, s);
})();
</script>


</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-mysqlForPrinciple1"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  Mysql架构与执行流程
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2021/08/22/mysqlForPrinciple1/" class="article-date">
  <time datetime="2021-08-22T08:48:49.000Z" itemprop="datePublished">2021-08-22</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/mysql/">mysql</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">7.7k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">27 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <p>Mysql 是广泛使用的关系型数据库，来学习一下Mysql的架构与执行流程</p>
<a id="more"></a>



<h1 id="一条SQL查询语句如何执行"><a href="#一条SQL查询语句如何执行" class="headerlink" title="一条SQL查询语句如何执行"></a>一条SQL查询语句如何执行</h1><p><img src="https://tu.dxysun.com/20210822174516-20210822174517.png" alt="">1、首先，客户端与服务端建立连接</p>
<p>2、查询缓存，msql内部自带了一个缓存模块，默认关闭，关闭的情况下跳过</p>
<p>3、对SQL语句进行词法语法解析，检查SQL语句是否正确，生成解析树</p>
<p>4、预处理器，检查经过解析后的SQL中的表名和字段是否存在，得到新的解析树</p>
<p>5、优化器，根据解析树生成不同的执行计划，选择最优的一条执行计划，优化器可以优化我们的SQL语句，决定是否走索引等</p>
<p>6、执行引擎根据执行计划调用存储引擎去执行SQL，最终把数据返回给客户端</p>
<h2 id="连接"><a href="#连接" class="headerlink" title="连接"></a>连接</h2><p>MySQL服务监听的端口默认是3306，客户端连接服务端的方式有很多。</p>
<p>可以是同步的也可以是异步的，可以是长连接也可以是短连接，可以是TCP也可以是UnixSocket，MySQL有专门处理连接的模块，连接的时候需要验证权限。</p>
<p>客户端每产生一个连接或者一个会话，在服务端就会创建一个线程来处理。</p>
<p>既然是分配线程的话，保持连接肯定会消耗服务端的资源，MySQL会把那些长时间不活动的（SLEEP）连接自动断开。</p>
<p>MySQL服务允许的最大连接数（也就是并发数）在5.7版本中默认是151个，最大可以设置成100000。下面的sql语句可以查看最大连接数</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'max_connections'</span>;</span><br></pre></td></tr></table></figure>

<p>参数级别说明：<br>MySQL中的参数（变量）分为session和global级别，分别是在当前会话中生效和全局生效，但是并不是每个参数都有两个级别，比如<code>max_connections</code>就只有全局级别。当没有带参数的时候，默认是session级别，包括查询和修改。</p>
<p>比如修改了一个参数以后，在本窗口查询已经生效，但是其他窗口不生效：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">VARIABLES</span> <span class="keyword">like</span> <span class="string">'autocommit'</span>; </span><br><span class="line"><span class="keyword">set</span> autocommit = <span class="keyword">on</span>;</span><br></pre></td></tr></table></figure>

<p>所以，如果只是临时修改，建议修改session级别。</p>
<p>如果需要在其他会话中生效，必须显式地加上global参数。</p>
<h2 id="查询缓存"><a href="#查询缓存" class="headerlink" title="查询缓存"></a>查询缓存</h2><p>MySQL的缓存默认是关闭的，可通过以下命令查询</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'query_cache%'</span>;</span><br></pre></td></tr></table></figure>

<p>默认关闭的意思就是不推荐使用，主要是因为MySQL自带的缓存的应用场景有限，第一个是它要求SQL语句必须一模一样，中间多一个空格，字母大小写不同都被认为是不同的的SQL。</p>
<p>第二个是表里面任何一条数据发生变化的时候，这张表所有缓存都会失效，所以对于有大量数据更新的应用，也不适合。</p>
<p>所以缓存这一块，我们还是交给ORM框架（比如MyBatis默认开启了一级缓存），或者独立的缓存服务，比如Redis来处理更合话。</p>
<p>在MySQL8.0中，查询缓存已经被移除了，</p>
<h2 id="词法语法解析器"><a href="#词法语法解析器" class="headerlink" title="词法语法解析器"></a>词法语法解析器</h2><p>词法语法分析会对SQL做一些语法检查，比如单引号有没有闭合，然后根据MySQL定义的语法规则，根据SQL语句生成一个数据结构。这个数据结构我们把它叫做解析树（select_lex）。</p>
<h2 id="预处理器"><a href="#预处理器" class="headerlink" title="预处理器"></a>预处理器</h2><p>预处理器会检查生成的解析树，解决解析器无法解析的语义。比如，它会检查表和列名是否存在，检查名字和别名，保证没有歧义。<br>预处理之后得到一个新的解析树。</p>
<h2 id="查询优化（Query-Optimizer）与查询执行计划"><a href="#查询优化（Query-Optimizer）与查询执行计划" class="headerlink" title="查询优化（Query Optimizer）与查询执行计划"></a>查询优化（Query Optimizer）与查询执行计划</h2><p>查询优化器的目的就是根据解析树生成不同的执行计划（Execution Plan），然后选择一种最优的执行计划，MySQL里面使用的是基于开销（cost）的优化器，那种执行计划开销最小，就用哪种。</p>
<p>MySQL提供了一个执行计划的工具。我们在SQL语句前面加上EXPLAIN，就可以看到执行计划的信息。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">EXPLAIN</span> <span class="keyword">select</span> <span class="keyword">name</span> <span class="keyword">from</span> <span class="keyword">user</span> <span class="keyword">where</span> <span class="keyword">id</span>=l;</span><br></pre></td></tr></table></figure>

<p>优化器最终会把解析树变成一个查询执行计划，查询执行计划是一个数据结构。</p>
<p>执行计划主要包括以下内容</p>
<p>比如多张表关联查询，先查询哪张表？在执行，查询的时候可能用到哪些索引，实际上用到了什么索引？</p>
<h2 id="存储引擎"><a href="#存储引擎" class="headerlink" title="存储引擎"></a>存储引擎</h2><p>没有指定存储引擎时，数据库就会使用默认的存储引擎，5.5.5之前，默认的存储引擎是MylSAM，5.5.5之后，默认的存储引擎是InnoDB。</p>
<p>一张表的仔储引擎，是在创建表的时候指定的，便用ENGINE关键字，例如</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="keyword">user</span> <span class="keyword">innodb</span>’</span><br><span class="line"><span class="keyword">id</span><span class="string">' int(ll) NOT NULL AUTO INCREMENT,</span></span><br><span class="line"><span class="string">name'</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">DEFAULT</span> <span class="literal">NULL</span></span><br><span class="line">gender<span class="string">' tinyint(l) DEFAULT NULL,</span></span><br><span class="line"><span class="string">phone'</span> <span class="built_in">varchar</span>(ll) <span class="keyword">DEFAULT</span> <span class="literal">NULL</span>,</span><br><span class="line">PRIMARY <span class="keyword">KEY</span> (<span class="keyword">id</span>),</span><br><span class="line"><span class="keyword">KEY</span> <span class="string">'comidx_name_phone'</span> (<span class="keyword">name</span><span class="string">','</span> phone<span class="string">')</span></span><br><span class="line"><span class="string">) ENGINE=InnoDB AUTO INCREMENT=l DEFAULT CHARSET  '</span>utf8mb4;</span><br></pre></td></tr></table></figure>

<p>常见的存储引擎比较参考官方文档  <a href="https://dev.mysql.com/doc/refman/5.7/en/storage-engines.h" target="_blank" rel="noopener">https://dev.mysql.com/doc/refman/5.7/en/storage-engines.h</a></p>
<p>简单比较一下 MylSAM 和 InnoDB</p>
<h3 id="MylSAM"><a href="#MylSAM" class="headerlink" title="MylSAM"></a>MylSAM</h3><p>应用范围比较小，表级锁定限制了读/写的性能，因此在Web和数据仓库配置中，它通常用于只读或以读为主的工作。</p>
<p>特点：</p>
<p>支持表级别的锁（插入和更新会锁表），不支持事务。</p>
<p>拥有较高的插入（insert）和查询（select）速度。</p>
<p>存储了表的行数（count速度更快）。</p>
<p>适合：</p>
<p>只读之类的数据分析的项目。</p>
<h3 id="InnoDB"><a href="#InnoDB" class="headerlink" title="InnoDB"></a>InnoDB</h3><p>mysq|5.7中的默认存储引擎。InnoDB是一个事务安全（与ACID兼容）的MySQL存储引擎，它具有提交、回滚和崩溃恢复功能来保护用户数据。InnoDB行级锁（不升级为更粗粒度的锁）和Oracle风格的一致非锁读提高了多用户并发性和性能。InnoDB将用户数据存储在聚集索引中，以减少基于主键的常见查询的1/O。为了保持数据完整性，InnoDB还支持外键引用完整性约束。</p>
<p>特点：<br>支持事务，支持外键，因此数据的完整性、一致性更高。</p>
<p>支持行级别的锁和表级别的锁。</p>
<p>支持读写并发，写不阻塞读（MVCC）。</p>
<p>特殊的索引存放方式，可以减少10，提升查询效率。</p>
<p>适合：经常更新的表，存在并发读写或者有事务处理的业务系统。</p>
<h2 id="执行引擎（Query-Execution-Engine），返回结果"><a href="#执行引擎（Query-Execution-Engine），返回结果" class="headerlink" title="执行引擎（Query Execution Engine），返回结果"></a>执行引擎（Query Execution Engine），返回结果</h2><p>存储引擎主要用于存储数据，执行引擎则利用存储引擎提供的相应的API来完成操作。</p>
<p>为什么我们修改了表的存储引擎，操作方式不需要做任何改变？</p>
<p>因为不同功能的存储引擎实现的API是相同的。</p>
<p>执行引擎最后把数据返回给客户端。</p>
<h1 id="Mysql架构"><a href="#Mysql架构" class="headerlink" title="Mysql架构"></a>Mysql架构</h1><p><img src="https://tu.dxysun.com/20210822180218-20210822180219.png" alt=""></p>
<p>总结一下，Mysql 的架构主要分为三层，连接层，服务层，存储引擎层。</p>
<p>连接层对应对应的上面执行过程中的连接，存储引擎层对应存储引擎，其余的都在服务层</p>
<h1 id="更新SQL如何执行"><a href="#更新SQL如何执行" class="headerlink" title="更新SQL如何执行"></a>更新SQL如何执行</h1><p>在数据库里面，更新操作其实包括了更新、插入和删除。更新流程和查询流程有什么不同呢？</p>
<p>其实基本流程也是一致的，也就是说，它也要经过解析器、优化器的处理，最后交给执行器。</p>
<p>区别就在于拿到符合条件的数据之后的操作。</p>
<p>因为更新操作涉及到数据存储。</p>
<h2 id="缓冲池Buffer-Pool"><a href="#缓冲池Buffer-Pool" class="headerlink" title="缓冲池Buffer Pool"></a>缓冲池Buffer Pool</h2><p>首先，对于InnoDB存储引擎来说，数据都是放在磁盘上的，存储引擎要操作数据，必须先把磁盘里面的数据加载到内存里面才可以操作。</p>
<p>这里就有个问题，是不是我们需要的数据多大，我们就一次从磁盘加载多少数据到内存呢？比如我要读6个字节。</p>
<p>磁盘I/O的读写相对于内存的操作来说是很慢的。如果需要的数据分散在磁盘的不同的地方，那就意味着会产生很多次的I/O操作。</p>
<p>无论是操作系统也好，还是存储引擎也好，都有一个预读取的概念。当磁盘上的一块数据被读取的时候，很有可能它附近的位置也会马上被读取到，这个就叫做局部性原理。所以InoDB每次都多读取一点，而不是用多少读多少。</p>
<p>InnoDB设定了一个存储引擎从磁盘读取数据到内存的最小的单位，叫做页。操作系统也有页的概念，操作系统的页大小一般是4K，而在InnoDB里面，这个最小的单位默认是16KB大小。如果要修改这个值的大小，需要清空数据重新初始化服务。</p>
<p>lnnoDB设计了一个内存的缓冲区。读取数据的时候，先判断是不是在这个内存区域里面，如果是，就直接读取，然后操作，不用再次从磁盘加载。如果不是，读取后就写到这个内存的缓冲区。这个内存区域有个专属的名字，叫Buffer Pool。</p>
<p><img src="https://tu.dxysun.com/20210822181616-20210822181617.png" alt=""></p>
<p>修改数据的时候，也是先写入到buffer pool，而不是直接写到磁盘。内存的数据页和磁盘数据不一致的时候，我们把它叫做脏页。那脏页什么时候才同步到磁盘呢？</p>
<p>InnoDB里面有专门的后台线程把Buffer Pool的数据写入到磁盘，每隔一段时间就一次性地把多个修改写入磁盘，这个动作就叫做刷脏。</p>
<p>总结一下：Buffer Pool的作用是为了提高读写的效率。</p>
<h2 id="Redo-log"><a href="#Redo-log" class="headerlink" title="Redo log"></a>Redo log</h2><p>因为刷脏不是实时的，如果Buffer Pool里面的脏页还没有刷入磁盘专时，数据库宕机或者重启，这些数据就会丢失。</p>
<p>那怎么办呢？所以内存的数据必须要有一个持久化的措施。</p>
<p>为了避免这个问题，InnoDB把所有对页面的修改操作专门写入一个日志文件。</p>
<p>如果有未同步到磁盘的数据，数据库在启动的时候，会从这个日志文件进行恢复操作（实现crash-safe）。</p>
<p>事务的ACID里面D（持久性），就是用它来实现的。</p>
<p><img src="https://tu.dxysun.com/image-20210822182132141-20210906233620.png" alt="image-20210822182132141"></p>
<p>这个日志文件就是磁盘的redolog（叫做重做日志）。</p>
<p>InnoDB 为什么不直接写到db file里面去？为什么先写日志再写磁盘？写日志文件和和写到数据文件有什么区别？</p>
<p>先说一下磁盘寻址的过程。这个是磁盘的构造。磁盘的盘片不停地旋转，磁头会在磁盘表面画出一个圆形轨迹，这个就叫磁道。从内到位半径不同有很多磁道。然后又用半径线，把磁道分割成了扇区（两根射线之内的扇区组成扇面）。如果要读写数据，必须找到数据对应的扇区，这个过程就叫寻址。</p>
<p><img src="https://tu.dxysun.com/image-20210822182347624-20210906233627.png" alt="image-20210822182347624"></p>
<p>如果所需要的数据是随机分散在磁盘上不同页的不同扇区中，那么找到相应的数据需要等到磁臂旋转到指定的页，然后盘片寻找到对应的扇区，才能找到我们所需要的一块数据，一次进行此过程直到找完所有数据，这个就是随机I/O，读取数据速度较慢。</p>
<p>假设已经找到了第一块数据，并且其他所需的数据就在这一块数据后边，那么就不需要重新寻址，可以依次拿到所需的数据，这个就叫顺序I/O。</p>
<p>刷盘是随机I/O，而记录日志是顺序I/O（连续写的），顺序/O效率更高，本质上是数据集中存储和分散存储的区别。</p>
<p>因此先把修改写入日志文件，在保证了内存数据的安全性的情况下，可以延迟刷盘时机，进而提升系统吞吐。</p>
<p>redolog位于/var/lib/mysqI/目录下的ib_logfile0和ib_logfile1，默认2个文件，每个48M。查询redolog相关配置</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'innodb_log%'</span>;</span><br></pre></td></tr></table></figure>

<p>redolog特点</p>
<p>1、redolog是InnoDB存储引擎实现的，并不是所有存储引擎都有，支持崩溃恢复是InnoDB的一个特性。</p>
<p>2、redolog不是记录数据页更新之后的状态，而是记录的是”在某个数据页上做了什么修改”，属于物理日志。</p>
<p>3、redoloq的大小是固定的，前面的内容会被覆盖，一旦写满，就会触发buffer pool 到磁盘的同步，以便腾出空间记录后面的修改。</p>
<p>除了redolog之外，还有一个跟修改有关的日志，叫做undolog，redolog和undolog与事务密切相关，统称为事务日志。</p>
<h2 id="Undo-log"><a href="#Undo-log" class="headerlink" title="Undo log"></a>Undo log</h2><p>undolog（撤销日志或回滚日志）记录了事务发生之前的数据状态，分为insert undo log和update undolog。如果修改数据时出现异常，可以用undolog来实现回滚操作（保持原子性）。</p>
<p>可以理解为undo log记录的是反向的操作，比如insert会记录delete，update会记录update原来的值，跟redolog记录在哪个物理页面做了什么操作不同，所以叫做逻辑格式的日志。</p>
<p>查询相关配置</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">global</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'%undo%'</span>;</span><br></pre></td></tr></table></figure>

<p>redo Log和undoLog与事务密切相关，统称为事务日志。</p>
<h2 id="更新过程"><a href="#更新过程" class="headerlink" title="更新过程"></a>更新过程</h2><p>总结一下一个更新操作的流程。</p>
<p>示例 name原值是shan。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> <span class="keyword">user</span> <span class="keyword">set</span> <span class="keyword">name</span> = <span class="string">'yan'</span> <span class="keyword">where</span> <span class="keyword">id</span>=<span class="number">1</span>;</span><br></pre></td></tr></table></figure>

<p>1、事务开始，从内存（buffer pool）或磁盘（data file）取到包含这条数据的数据页，返回给Server的执行器；</p>
<p>2、Server的执行器修改数据页的这一行数据的值为yan；</p>
<p>3、记录name=shan到undo log；</p>
<p>4、记录name=yan到redo log；</p>
<p>5、调用存储引擎接口，记录数据页到Buffer Pool（修改name=yan）</p>
<p>6、事务提交。</p>
<h1 id="InnoDB总体架构"><a href="#InnoDB总体架构" class="headerlink" title="InnoDB总体架构"></a>InnoDB总体架构</h1><p><img src="https://tu.dxysun.com/image-20210822195802480-20210823220517.png" alt="image-20210822195802480"></p>
<h2 id="内存结构"><a href="#内存结构" class="headerlink" title="内存结构"></a>内存结构</h2><p>Buffer Pool主要分为3个部分： Buffer Pool、 Change Buffer、 Adaptive Hash Index，另外还有一个 (redo) log buffer。</p>
<h3 id="Buffer-Pool"><a href="#Buffer-Pool" class="headerlink" title="Buffer Pool"></a>Buffer Pool</h3><p>Buffer Pool缓存的是页面信息，包括数据页、索引页。</p>
<p>Buffer Pool默认大小是128M（134217728字节），可以调整。</p>
<p>查看系统变量：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="keyword">VARIABLES</span> <span class="keyword">like</span> <span class="string">'%innodb_buffer_ pool%'</span>;</span><br></pre></td></tr></table></figure>

<p>查看服务器状态，里面有很多跟Buffer Pool相关的信息：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="keyword">STATUS</span> <span class="keyword">LIKE</span> <span class="string">'%innodb_buffer_pool%'</span>;</span><br></pre></td></tr></table></figure>

<p>内存的缓冲池写满了怎么办？</p>
<p>InnoDB用LRU算法来管理缓冲池（链表实现，不是传统的LRU，分成了young和old），经过淘汰后的数据就是热点数据。</p>
<h3 id="LRU"><a href="#LRU" class="headerlink" title="LRU"></a>LRU</h3><p>InnoDB中使用了一个双向链表，LRU list。但是这个LRUlist放的不是data page，而是指向缓存页的指针。</p>
<p>如果写buffer pool的时候发现没有空闲页了，就要从buffer pool中淘汰数据页了，它要根据LRU链表的数据来操作。</p>
<p>首先，InnoDB的数据页并不是都是在访问的时候才缓存到buffer pool的。</p>
<p>InnoDB有一个预读机制（read ahead），也就是说，设计者认为访问某个page的数据的时候，相邻的一些page可能会很快被访问到，所以先把这些page放到buffer pool中缓存起来。</p>
<p>这种预读的机制又分为两种类型，一种叫线性预读（异步的）（Linear read-ahead）。为了便于管理，InnoDB中把64个相邻的page叫做一个extent（区）。如果顺序地访问了一个extent的56个page，这个时候InnoDB就会把下一个extent（区）缓存到buffer pool中。<br>顺序访问多少个page才缓存下一个extent，由一个参数控制：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'innodb_read_ahead_threshold'</span>;</span><br></pre></td></tr></table></figure>

<p>第二种叫做随机预读（Random read-ahead），如果buffer pool已经缓存了同一个extent（区）的数据页的个数超过13时，就会把这个extent剩余的所有page全部缓存到buffer pool。</p>
<p>但是随机预读的功能默认是不启用的，由一个参数控制：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'innodb random read ahead'</span>;</span><br></pre></td></tr></table></figure>

<p>很明显，线性预读或者随机预读，能够把可能即将用到的数据提前加载到 buffer pool，肯定能提升I/O的性能，所以是一种非常有用的机制。但是预读肯定也会带来一些副作用，就是导致占用的内存空间更多，剩余的空闲页更少。如果说buffer pool size不是很大，而预读的数据很多，很有可能那些真正的需要被缓存的热点数据被预读的数据挤出 buffer pool，淘汰掉了，下次访问的时候又要先去磁盘。</p>
<p>怎么让这些真正的热点数据不受到预读的数据的影响呢？</p>
<p>InnoDB 把LRU list分成两部分，靠近head的叫做new sublist用来放热数据（我们把它叫做热区），靠近tail的叫做old sublist，用来放冷数据（我们把它叫做冷区），中间的分割线叫做midpoint，也就是对buffer pool做个冷热分离。</p>
<p><img src="https://tu.dxysun.com/20210822214424-20210822214424.png" alt=""></p>
<p>所有新数据加入到buffer pool的时候，一律先放到冷数据区的head，不管是预读的，还是普通的读操作，所以如果有一些预读的数据没有被用到，会在oldsublist（冷区）直接被淘汰。放到LRU List以后，如果再次被访问，都把它移动到热区的head。如果热区的数据长时间没有被访问，会被先移动到冷区的head部，最后慢慢在tail被淘汰。</p>
<p>在默认情况下，热区占了5/8的大小，冷区占了3/8，这个值由 innodb_old_blocks_pct控制，它代表的是old区的大小，默认是37%，也就是3/8，innodb_old_blocks_pct的值可以调整，在5%到95%之间，这个值越大，new 区越小，这个LRU算法就接近传统LRU。</p>
<p>如果这个值太小，old区没有被访问的时候，淘汰会更快。</p>
<p>预读的问题，通过冷热分离解决了，还有没有其他的问题呢？</p>
<p>我们先把数据放到冷区，用来避免占用热数据的存储空间，如果加载的冷区数据立即被访问了一次，按照原来的逻辑，这个时候会马上把它移动到热区。</p>
<p>假设这一次加载然后被立即访问的冷区数据量非常大，比如我们查询了一张几千万数据的大表，没有使用索引，做了一个全表扫描。或者，dump全表备份数据，这种查询属于短时间内访问，后面再也不会用到了。</p>
<p>如果短时间之内被访问了一次，导致它们全部被移动到热区的head，它会导致很多热点数据被移动到冷区甚至被淘汰，造成了缓冲池的污染。</p>
<p>这个问题我们又怎么解决呢？</p>
<p>InnoDB 对于加载到冷区然后被访问的数据，设置个时间窗，只有超过这个时间之后被访问，才认为它是有效的访问。</p>
<p>InnoDB里面通过innodb_old_blocks_time这个参数来控制，默认是1秒钟。也就是说1秒钟之内被访问的，不算有效访问，待在冷区不动。只有1秒钟以后被访问的才从冷区移动到热区的head，这样就可以从很大程度上避免全表扫描或者预读的数据污染真正的热数据。</p>
<p>同时为了避免并发的问题，对于LRU链表的操作是要加锁的。也就是说每一次链表的移动，都会带来资源的竞争和等待。从这个角度来说，如果要进一步提升InnoDB LRU的效率，就要尽量地减少LRU链表的移动。</p>
<p>比如，把热区一个非常靠近head的page移动到head，有没有这个必要呢？</p>
<p>所以InnoDB对于new区还有一个特殊的优化：</p>
<p>如果一个缓存页处于热数据区域，且在热数据区域的前1/4区域（注意是热数据区域的1/4，不是整个链表的1/4），那么当访问这个缓存页的时候，就不用把它移动到热数据区域的头部；如果缓存页处于热区的后3/4区域，那么当访问这个缓存页的时候，会把它移动到热区的头部。</p>
<p>内存缓冲区对于提升读写性能有很大的作用。</p>
<p>思考一个问题：</p>
<p>当需要更新一个数据页时，如果数据页在 BufferPool 中存在，那么就直接更新好了。否则的话就需要从磁盘加载到内存，再对内存的数据页进行操作。也就是说，如果没有命中缓存池，至少要产生一次磁盘I/O，有没有优化的方式呢？</p>
<h3 id="Change-Buffer写缓冲"><a href="#Change-Buffer写缓冲" class="headerlink" title="Change Buffer写缓冲"></a>Change Buffer写缓冲</h3><p>Change Buffer是Buffer Pool的一部分。</p>
<p>如果这个数据页不是唯一索引，不存在需要判断数据重复的情况，也就不需要从磁盘加载索引页判断数据是不是重复（唯一性检查）。这种情况下可以先把修改记录在内存的缓冲池中，从而提升更新语句（Insert、Delete、Update）的执行速度。</p>
<p>这一块区域就是Change Buffer。5.5之前叫Insert Buffer插入缓冲，现在也能支持delete和update。</p>
<p>最后把Change Buffer记录到数据页的操作叫做merge。什么时候发生merge？有几种情况：在访问这个数据页的时候，或者通过后台线程、或者数据库shut downredolog写满时触发。</p>
<p>如果数据库大部分索引都是非唯一索引，并且业务是写多读少，不会在写数据后立刻读取，就可以使用Change Bufter（写缓冲）。</p>
<p>可以通过调大这个值，来扩大Change的大小，以支持写多读少的业务场景。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="keyword">VARIABLES</span> <span class="keyword">LIKE</span> <span class="string">'innodb_change_buffer_max_size'</span>;</span><br></pre></td></tr></table></figure>

<p>代表Change Buffer占Buffer Pool的比例，默认25%。</p>
<h3 id="Adaptive-Hash-Index"><a href="#Adaptive-Hash-Index" class="headerlink" title="Adaptive Hash Index"></a>Adaptive Hash Index</h3><p>存放一种哈希的索引</p>
<h3 id="Redo-Log-Buffer"><a href="#Redo-Log-Buffer" class="headerlink" title="Redo Log Buffer"></a>Redo Log Buffer</h3><p>Redo log也不是每一次都直接写入磁盘的，Buffer Pool里面有一块内存区域（Log Buffer）专门用来保存即将要写入日志文件的数据，默认16M，它一样可以节省磁盘IO。</p>
<p><img src="https://tu.dxysun.com/image-20210822222424971-20210906233645.png" alt="image-20210822222424971"></p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHow</span> <span class="keyword">VARIABLES</span> <span class="keyword">LIKE</span> <span class="string">'innodb_log_buffer_size'</span>;</span><br></pre></td></tr></table></figure>

<p>需要注意：redolog的内容主要是用于崩溃恢复，磁盘的数据文件，数据来自buffer pool， redo log写入磁盘，不是写入数据文件。</p>
<p>在我们写入数据到磁盘的时候，操作系统本身是有缓存的。flush就是把操作系统缓冲区写入到磁盘。</p>
<p>log buffer写入磁盘的时机，由一个参数控制，默认是1。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="keyword">VARIABLES</span> <span class="keyword">LIKE</span> <span class="string">'innodb_flush_log_at_trx_commit'</span>;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>0（延迟写）log buffer将每秒一次地写入log file中，并且log file的flush操作同时进行。 该模式下，在事务提交的时候，不会主动触发写入磁盘的操作。<br>1（默认），实时每次事务提交时MySQL都会把log buffer的数据写入log file，并且刷到磁盘写，实时刷到磁盘中去。<br>2（实时写，延迟刷）每次事务提交时MySQL都会把log buffer的数据写入log file。但是flush操作并不会同时进行。该模式下，MySQL会每秒执行一次flush操作。</p>
</blockquote>
<p><img src="https://tu.dxysun.com/image-20210822224615948-20210906233655.png" alt="image-20210822224615948"></p>
<p>刷盘越快，越安全，但是也会越消耗性能。</p>
<h2 id="磁盘结构"><a href="#磁盘结构" class="headerlink" title="磁盘结构"></a>磁盘结构</h2><p>表空间可以看做是InnoDB存储引擎逻辑结构的最高层，所有的数据都存放在表空间中，InnoDB的表空间分为5大类。</p>
<p>系统表空间 system tablespace</p>
<p>独占表空间 file-per- table tablespaces</p>
<p>通用表空间 general tablespaces</p>
<p>临时表空间 temporary table spaces</p>
<p>Undo 表空间</p>
<h3 id="系统表空间system-tablespace"><a href="#系统表空间system-tablespace" class="headerlink" title="系统表空间system tablespace"></a>系统表空间system tablespace</h3><p>在默认情况下InnoDB存储引擎有一个共享表空间，也叫系统表空间（对应文件/var/lib/mysqI/ibdata1）。</p>
<p>lnnoDB系统表空间包含InnoDB数据字典和双写缓冲区，Change Buffer和Undo Logs，如果没有指定 file-per-table，也包含用户创建的表和索引数据。</p>
<p>1、undo log 也可以设置独立的表空间</p>
<p>2、数据字典：由内部系统表组成，存储表和索引的元数据（定义信息）</p>
<p>3、双写缓冲（InnoDB的一大特性）</p>
<p>lnnoDB的页和操作系统的页大小不一致，InnoDB页大小一般为16K，操作系统页大小为4K，InnoDB的页写入到磁盘时，一个页需要分4次写。</p>
<p><img src="https://tu.dxysun.com/20210822225329-20210822225330.png" alt=""></p>
<p>如果存储引擎正在写入页的数据到磁盘时发生了宕机，可能出现页只写了一部分的情况，比如只写了4K，就宕机了，这种情况叫做部分写失效（partial page write），可能会导致数据丢失。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> <span class="keyword">variables</span> <span class="keyword">like</span> <span class="string">'innodb_doublewrite'</span>;</span><br></pre></td></tr></table></figure>

<p>我们不是有redo log吗？但是有个问题，如果这个页本身已经损坏了，用它来做崩溃恢复是没有意义的。所以在对于应用redolog之前，需要一个页的副本。如果出现了写入失效，就用页的副本来还原这个页，然后再应用redo log，这个页的副本就是doublewrite，InnoDB的双写技术，通过它实现了数据页的可靠性。</p>
<p>跟redo log一样，double write由两部分组成，一部分是内存的double write，一个部分是磁盘上的double write。因为double write是顺序写入的，不会带来很大的开销。</p>
<p>在默认情况下，所有的表共享一个系统表空间，这个文件会越来越大，而且它的空间不会收缩。</p>
<h3 id="独占表空间file-per-table-tablespaces"><a href="#独占表空间file-per-table-tablespaces" class="headerlink" title="独占表空间file-per-table tablespaces"></a>独占表空间file-per-table tablespaces</h3><p>我们可以让每张表独占一个表空间。这个开关通过innodb_file_per_table设置，默认开启。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> <span class="keyword">VARIABLES</span> <span class="keyword">LIKE</span> <span class="string">'innodb_file_per_table'</span>;</span><br></pre></td></tr></table></figure>

<p>开启后，则每张表会开辟一个表空间，这个文件就是数据目录下的ibd文件（例如/var/lib/mysql/gupao/user_innodb.ibd），存放表的索引和数据。</p>
<p>但是其他类的数据，如回滚（undo）信息，插入缓冲索引页、系统事务信息，二次写缓冲（Double write buffer）等还是存放在原来的共享表空间内。</p>
<h3 id="通用表空间-general-tablespaces"><a href="#通用表空间-general-tablespaces" class="headerlink" title="通用表空间 general tablespaces"></a>通用表空间 general tablespaces</h3><p>通用表空间也是一种共享的表空间，跟ibdata1类似。<br>可以创建一个通用的表空间，用来存储不同数据库的表，数据路径和文件可以自定义。语法：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">create</span> <span class="keyword">tablespace</span> ts2673 <span class="keyword">add</span> datafle <span class="string">'/varib/mysql/ts2673.ibd'</span> file_block_size=<span class="number">16</span>K <span class="keyword">engine</span>=<span class="keyword">innodb</span>;</span><br></pre></td></tr></table></figure>

<p>泡<br>在创建表的时候可以指定表空间，用ALTER修改表空间可以转移表空间。</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">create</span> <span class="keyword">table</span> t2673(<span class="keyword">id</span> <span class="built_in">integer</span>) <span class="keyword">tablespace</span> ts2673;</span><br></pre></td></tr></table></figure>

<p>不同表空间的数据是可以移动的，删除表空间需要先删除里面的所有表：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">drop</span> <span class="keyword">table</span> t2673;</span><br><span class="line"><span class="keyword">drop</span> <span class="keyword">tablespace</span> ts2673;</span><br></pre></td></tr></table></figure>



<h3 id="临时表空间temporary-tablespaces"><a href="#临时表空间temporary-tablespaces" class="headerlink" title="临时表空间temporary tablespaces"></a>临时表空间temporary tablespaces</h3><p>存储临时表的数据，包括用户创建的临时表，和磁盘的内部临时表。对应数据目录下的ibtmp1文件。当数据服务器正常关闭时，该表空间被删除，下次重新产生。</p>
<h3 id="undo-log-tablespace"><a href="#undo-log-tablespace" class="headerlink" title="undo log tablespace"></a>undo log tablespace</h3><p>undo Log的数据默认在系统表空间ibdata1文件中，因为共享表空间不会自动收缩，也可以单独创建个undo表空间。</p>
<h2 id="后台线程"><a href="#后台线程" class="headerlink" title="后台线程"></a>后台线程</h2><p>后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。</p>
<p>后台线程分为：master thread，IO thread，purge thread，page cleaner thread。</p>
<p>master thread 负责刷新缓存数据到磁盘并协调调度其它后台进程。</p>
<p>lO thread 分为insert buffer、log 、read、write进程，分别用来处理insert buffer、重做日志、读写请求的IO回调。</p>
<p>purge thread用来回收undo页。</p>
<p>page cleaner thread用来刷新脏页。</p>
<p>除了InnoDB架构中的日志文件，MySQL的Server层也有一个日志文件，叫做binlog，它可以被所有的存储引擎使用。</p>
<h2 id="Binlog"><a href="#Binlog" class="headerlink" title="Binlog"></a>Binlog</h2><p>binlog以事件的形式记录了所有的DDL和DML语句（因为它记录的是操作而不是数据值，属于逻辑日志），可以用来做主从复制和数据恢复。</p>
<p>跟redolog不一样，它的文件内容是可以追加的，没有固定大小限制。</p>
<p>在开启了binlog功能的情况下，我们可以把binlog导出成SQL语句，把所有的操作重放一遍，来实现数据的恢复。</p>
<p>binlog的另一个功能就是用来实现主从复制，它的原理就是从服务器读取主服务器的binlog，然后执行一遍。</p>
<p>有了这两个日志之后，我们来看一下一条更新语句是怎么执行的（redo不能一次写入了）：</p>
<p><img src="https://tu.dxysun.com/image-20210822231523918-20210906233709.png" alt="image-20210822231523918"></p>
<p>例如一条语句：<code>update teacher set name=&#39;盆鱼宴&#39; where id=1;</code></p>
<p>1、先查询到这条数据，如果有缓存，也会用到缓存。</p>
<p>2、把name改成盆鱼宴，然后调用引擎的API接口，写入这一行数据到内存同时记录redolog。这时redolog进入prepare状态，然后告诉执行器，执行完成了，可以随时提交。</p>
<p>3、执行器收到通知后记录binlog，然后调用存储引擎接口，设置redolog为commit状态。</p>
<p>4、更新完成。</p>
<p>重点：</p>
<p>1、先记录到内存，再写日志文件。 </p>
<p>2、记录redolog分为两个阶段。</p>
<p>3、存储引擎和Server记录不同的日志。</p>
<p> 3、先记录redolog，再记录binlog。</p>
<p>为什么需要两阶段提交？</p>
<p>如果我们执行的是把name改成盆鱼宴，如果写完redolog，还没有写binlog的时候，MySQL重启了。</p>
<p>因为 redolog 可以在重启的时候用于恢复数据，所以写入磁盘的是盆鱼宴。但是binlog里面没有记录这个逻辑日志，所以这时候用binlog去恢复数据或者同步到从库，就会出现数据不一致的情况。</p>
<p>所以在写两个日志的情况下，binlog就充当了一个事务的协调者。通知InnoDB来执行prepare或者commit或者rollback。</p>
<p>如果第⑥步写入binlog失败，就不会提交。</p>
<p>简单地来说，这里有两个写日志的操作，类似于分布式事务，不用两阶段提交，就不能保证都成功或者都失败。</p>
<p>在崩溃恢复时，判断事务是否需要提交：</p>
<p>1、binlog无记录，redolog无记录：在redolog写之前崩溃，恢复操作，回滚事务</p>
<p>2、binlog无记录，redolog状态prepare：在binlog写完之前的崩溃，恢复操作：回滚事务</p>
<p>3、binlog有记录，redolog状态prepare：在binlog写完提交事务之前的崩溃，恢复操作：提交事务</p>
<p>4、binlog有记录，redolog状态commit：正常完成的事务，不需要恢复</p>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://dxysun.com/2021/08/22/mysqlForPrinciple1/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/mysql/" rel="tag">mysql</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/2021/08/22/mysqlForPrinciple2/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            Mysql原理之索引
          
        </div>
      </a>
    
    
      <a href="/2021/05/23/mybatisForSource4/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">MyBatis源码解读(四)执行SQL</div>
      </a>
    
  </nav>

  
   
  
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2015-2024
        <i class="ri-heart-fill heart_icon"></i> dxysun
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
        <li>
          <a href="https://beian.miit.gov.cn" target="_black" rel="nofollow">豫ICP备17012675号-1</a>
        </li>
        
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="https://dxysun.com/static/logo.png" alt="迎着朝阳"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/photos">相册</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">友链</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/about">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    <div class="reward-item">
      <img class="reward-img" src="https://tu.dxysun.com/alipay-20201219151322.jpg">
      <span class="reward-type">支付宝</span>
    </div>
    
    
    <div class="reward-item">
      <img class="reward-img" src="https://tu.dxysun.com/weixin-20201219151346.png">
      <span class="reward-type">微信</span>
    </div>
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
      tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
          processEscapes: true,
          skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
      }
  });

  MathJax.Hub.Queue(function() {
      var all = MathJax.Hub.getAllJax(), i;
      for(i=0; i < all.length; i += 1) {
          all[i].SourceElement().parentNode.className += ' has-jax';
      }
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.6/unpacked/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
  var ayerConfig = {
    mathjax: true
  }
</script>

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


<script src="/js/dz.js"></script>



    
  </div>
</body>

</html>